package com.own.compoent.util.zip.compress.util;

import com.own.constant.ConstantFileType;
import lombok.Cleanup;
import lombok.extern.slf4j.Slf4j;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

/**
 * ZipUtil
 *
 * @author chenxueli
 * @date 2021/4/20 8:53
 */
@Slf4j
public final class CompressUtil {

    private static final int BUFFER_SIZE = 2 * 1024;

    /**
     * 指定文件夹压缩成zip
     *
     * @param srcDir 压缩文件夹路径
     * @return 输入文件路径
     */
    public String compress(String srcDir) {
        // 获取最后一节的文件名称
        var index = srcDir.lastIndexOf(File.separator);
        // 需要去除文件名最前面的斜杠
        var name = srcDir.substring(index + 1);
        // 构造新的文件名称
        var outFile = srcDir + File.separator + name + "." + ConstantFileType.ZIP;
        return compress(srcDir, outFile);
    }

    /**
     * 指定文件夹压缩成zip（包括空文件夹）
     *
     * @param srcDir  压缩文件夹路径
     * @param outFile 输出文件全路径
     * @return 输入文件路径
     */
    public String compress(String srcDir, String outFile) {
        try {
            var out = new FileOutputStream(outFile);
            compress(srcDir, out, outFile);
        } catch (FileNotFoundException e) {
            throw new RuntimeException("文件压缩失败");
        }
        return outFile;
    }

    /**
     * 指定文件夹压缩成zip（包括空文件夹）
     *
     * @param srcDir 压缩文件夹路径
     * @param out    压缩文件输出流
     */
    public void compress(String srcDir, OutputStream out) {
        compress(srcDir, out, true);
    }

    /**
     * 指定文件夹压缩成zip（包括空文件夹）
     *
     * @param srcDir   压缩文件夹路径
     * @param out      压缩文件输出流
     * @param skipPath 跳过的文件
     */
    public void compress(String srcDir, OutputStream out, String skipPath) {
        compress(srcDir, out, skipPath, true);
    }

    /**
     * 指定文件夹压缩成zip
     *
     * @param srcDir           压缩文件夹路径
     * @param out              压缩文件输出流
     * @param keepDirStructure 是否保留原来的目录结构 true:保留目录结构; false:所有文件跑到压缩包根目录下(注意：不保留目录结构可能会出现同名文件,会压缩失败)
     */
    public void compress(String srcDir, OutputStream out, boolean keepDirStructure) {
        compress(srcDir, out, null, keepDirStructure);
    }

    /**
     * 指定文件夹压缩成zip
     *
     * @param srcDir           压缩文件夹路径
     * @param out              压缩文件输出流
     * @param skipPath         跳过的文件
     * @param keepDirStructure 是否保留原来的目录结构 true:保留目录结构; false:所有文件跑到压缩包根目录下(注意：不保留目录结构可能会出现同名文件,会压缩失败)
     */
    public void compress(String srcDir, OutputStream out, String skipPath, boolean keepDirStructure) {
        var start = System.currentTimeMillis();
        try {
            @Cleanup
            var zos = new ZipOutputStream(out);
            var sourceFile = new File(srcDir);
            compress(sourceFile, zos, sourceFile.getName(), skipPath, keepDirStructure);
            log.info("压缩完成，耗时：{} ms", System.currentTimeMillis() - start);
        } catch (Exception e) {
            throw new RuntimeException("zip error from ZipUtil", e);
        }
    }

    /**
     * 文件打包成一个zip
     *
     * @param file 需要压缩的文件列表
     * @param out  压缩文件输出流
     */
    public void compress(File file, OutputStream out) {
        var list = new ArrayList<File>();
        list.add(file);
        compress(list, out);
    }

    /**
     * 多个文件打包成一个zip
     *
     * @param fileList 需要压缩的文件列表
     * @param out      压缩文件输出流
     */
    public void compress(List<File> fileList, OutputStream out) {
        var start = System.currentTimeMillis();
        try {
            @Cleanup
            var zos = new ZipOutputStream(out);
            for (var srcFile : fileList) {
                var buffer = new byte[BUFFER_SIZE];
                zos.putNextEntry(new ZipEntry(srcFile.getName()));
                int len;
                @Cleanup
                var in = new FileInputStream(srcFile);
                while ((len = in.read(buffer)) != -1) {
                    zos.write(buffer, 0, len);
                }
                zos.closeEntry();
                in.close();
            }
            log.info("压缩完成，耗时：{} ms", System.currentTimeMillis() - start);
        } catch (Exception e) {
            throw new RuntimeException("zip error from ZipUtils", e);
        }
    }

    /**
     * 递归压缩方法
     *
     * @param sourceFile       源文件
     * @param zos              zip输出流
     * @param name             压缩后的名称
     * @param skipPath         跳过的文件
     * @param keepDirStructure 是否保留原来的目录结构 true:保留目录结构; false:所有文件跑到压缩包根目录下(注意：不保留目录结构可能会出现同名文件,会压缩失败)
     */
    private void compress(File sourceFile, ZipOutputStream zos, String name, String skipPath, boolean keepDirStructure) throws Exception {
        var buffer = new byte[BUFFER_SIZE];
        if (sourceFile.isFile()) {
            // 向zip输出流中添加一个zip实体，构造器中name为zip实体的文件的名字
            zos.putNextEntry(new ZipEntry(name));
            // copy文件到zip输出流中
            int len;
            @Cleanup
            var in = new FileInputStream(sourceFile);
            while ((len = in.read(buffer)) != -1) {
                zos.write(buffer, 0, len);
            }
            // Complete the entry
            zos.closeEntry();
            in.close();
        } else {
            var listFiles = sourceFile.listFiles();
            if (listFiles == null || listFiles.length == 0) {
                // 需要保留原来的文件结构时,需要对空文件夹进行处理
                if (keepDirStructure) {
                    // 空文件夹的处理
                    zos.putNextEntry(new ZipEntry(name + File.separator));
                    // 没有文件，不需要文件的copy
                    zos.closeEntry();
                }
            } else {
                for (var file : listFiles) {
                    // 跳过文件本身的子级
                    if (skipPath != null && skipPath.equals(file.getAbsolutePath())) {
                        continue;
                    }
                    // 判断是否需要保留原来的文件结构
                    // 注意：file.getName()前面需要带上父文件夹的名字加一斜杠,
                    // 不然最后压缩包中就不能保留原来的文件结构,即：所有文件都跑到压缩包根目录下了
                    compress(file, zos, (keepDirStructure ? name + File.separator : "") + file.getName(), skipPath, keepDirStructure);
                }
            }
        }
    }

}
